import { confirm } from "@inquirer/prompts";
import { createHash } from "crypto";
import type { Zippable } from "fflate";
import { zipSync } from "fflate";
import {
  existsSync,
  mkdirSync,
  readFileSync,
  statSync,
  writeFileSync,
} from "fs";
import { basename, join, relative, resolve, sep } from "path";

import { getAllFilesWithCount, readMcpbIgnorePatterns } from "../node/files.js";
import { validateManifest } from "../node/validate.js";
import { MANIFEST_SCHEMAS } from "../shared/constants.js";
import { getLogger } from "../shared/log.js";
import { getManifestVersionFromRawData } from "../shared/manifestVersionResolve.js";
import { initExtension } from "./init.js";

interface PackOptions {
  extensionPath: string;
  outputPath?: string;
  silent?: boolean;
}

function formatFileSize(bytes: number): string {
  if (bytes < 1024) {
    return `${bytes}B`;
  } else if (bytes < 1024 * 1024) {
    return `${(bytes / 1024).toFixed(1)}kB`;
  } else {
    return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
  }
}

function sanitizeNameForFilename(name: string): string {
  // Replace spaces with hyphens
  // Remove or replace characters that are problematic in filenames
  return name
    .toLowerCase()
    .replace(/\s+/g, "-") // Replace spaces with hyphens
    .replace(/[^a-z0-9-_.]/g, "") // Keep only alphanumeric, hyphens, underscores, and dots
    .replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
    .replace(/^-+|-+$/g, "") // Remove leading/trailing hyphens
    .substring(0, 100); // Limit length to 100 characters
}

export async function packExtension({
  extensionPath,
  outputPath,
  silent,
}: PackOptions): Promise<boolean> {
  const resolvedPath = resolve(extensionPath);
  const logger = getLogger({ silent });

  // Check if directory exists
  if (!existsSync(resolvedPath) || !statSync(resolvedPath).isDirectory()) {
    logger.error(`ERROR: Directory not found: ${extensionPath}`);
    return false;
  }

  // Check if manifest exists
  const manifestPath = join(resolvedPath, "manifest.json");
  if (!existsSync(manifestPath)) {
    logger.log(`No manifest.json found in ${extensionPath}`);
    const shouldInit = await confirm({
      message: "Would you like to create a manifest.json file?",
      default: true,
    });

    if (shouldInit) {
      const success = await initExtension(extensionPath);
      if (!success) {
        logger.error("ERROR: Failed to create manifest");
        return false;
      }
    } else {
      logger.error("ERROR: Cannot pack extension without manifest.json");
      return false;
    }
  }

  // Validate manifest first
  logger.log("Validating manifest...");
  if (!validateManifest(manifestPath)) {
    logger.error("ERROR: Cannot pack extension with invalid manifest");
    return false;
  }

  // Read and parse manifest
  let manifest;
  try {
    const manifestContent = readFileSync(manifestPath, "utf-8");
    const manifestData = JSON.parse(manifestContent);
    const manifestVersion = getManifestVersionFromRawData(manifestData);
    if (!manifestVersion) {
      logger.error(
        `ERROR: Manifest version mismatch. Expected "${Object.keys(MANIFEST_SCHEMAS).join(" or ")}", found "${manifestVersion}"`,
      );
      logger.error(
        `  Please update the manifest_version in your manifest.json to a supported version`,
      );
      return false;
    }

    manifest = MANIFEST_SCHEMAS[manifestVersion].parse(manifestData);
  } catch (error) {
    logger.error("ERROR: Failed to parse manifest.json");
    if (error instanceof Error) {
      logger.error(`  ${error.message}`);
    }
    return false;
  }

  // Determine output path
  const extensionName = basename(resolvedPath);
  const finalOutputPath = outputPath
    ? resolve(outputPath)
    : resolve(`${extensionName}.mcpb`);

  // Ensure output directory exists
  const outputDir = join(finalOutputPath, "..");
  mkdirSync(outputDir, { recursive: true });

  try {
    // Read .mcpbignore patterns if present
    const mcpbIgnorePatterns = readMcpbIgnorePatterns(resolvedPath);

    // Get all files in the extension directory
    const { files, ignoredCount } = getAllFilesWithCount(
      resolvedPath,
      resolvedPath,
      {},
      mcpbIgnorePatterns,
    );

    // Print package header
    logger.log(`\n📦  ${manifest.name}@${manifest.version}`);

    // Print file list
    logger.log("Archive Contents");
    const fileEntries = Object.entries(files);
    let totalUnpackedSize = 0;

    // Sort files for consistent output
    fileEntries.sort(([a], [b]) => a.localeCompare(b));

    // Group files by directory for deep nesting
    const directoryGroups = new Map<
      string,
      { files: string[]; totalSize: number }
    >();
    const shallowFiles: Array<{ path: string; size: number }> = [];

    for (const [filePath, fileData] of fileEntries) {
      const relPath = relative(resolvedPath, filePath);
      const content = fileData.data;
      const size =
        typeof content === "string"
          ? Buffer.byteLength(content, "utf8")
          : content.length;
      totalUnpackedSize += size;

      // Check if file is deeply nested (3+ levels)
      const parts = relPath.split(sep);
      if (parts.length > 3) {
        // Group by the first 3 directory levels
        const groupKey = parts.slice(0, 3).join("/");
        if (!directoryGroups.has(groupKey)) {
          directoryGroups.set(groupKey, { files: [], totalSize: 0 });
        }
        const group = directoryGroups.get(groupKey)!;
        group.files.push(relPath);
        group.totalSize += size;
      } else {
        shallowFiles.push({ path: relPath, size });
      }
    }

    // Print shallow files first
    for (const { path, size } of shallowFiles) {
      logger.log(`${formatFileSize(size).padStart(8)} ${path}`);
    }

    // Print grouped directories
    for (const [dir, { files, totalSize }] of directoryGroups) {
      if (files.length === 1) {
        // If only one file in the group, print it normally
        const filePath = files[0];
        const fileSize = totalSize;
        logger.log(`${formatFileSize(fileSize).padStart(8)} ${filePath}`);
      } else {
        // Print directory summary
        logger.log(
          `${formatFileSize(totalSize).padStart(8)} ${dir}/ [and ${files.length} more files]`,
        );
      }
    }

    // Create zip with preserved file permissions
    const zipFiles: Zippable = {};

    const isUnix = process.platform !== "win32";

    for (const [filePath, fileData] of Object.entries(files)) {
      if (isUnix) {
        // Set external file attributes to preserve Unix permissions
        // The mode needs to be shifted to the upper 16 bits for ZIP format
        zipFiles[filePath] = [
          fileData.data,
          { os: 3, attrs: (fileData.mode & 0o777) << 16 },
        ];
      } else {
        // On Windows, use default ZIP attributes (no Unix permissions)
        zipFiles[filePath] = fileData.data;
      }
    }

    const zipData = zipSync(zipFiles, {
      level: 9, // Maximum compression
      mtime: new Date(),
    });

    // Write zip file
    writeFileSync(finalOutputPath, zipData);

    // Calculate SHA sum
    const shasum = createHash("sha1").update(zipData).digest("hex");

    // Print archive details
    const sanitizedName = sanitizeNameForFilename(manifest.name);
    const archiveName = `${sanitizedName}-${manifest.version}.mcpb`;
    logger.log("\nArchive Details");
    logger.log(`name: ${manifest.name}`);
    logger.log(`version: ${manifest.version}`);
    logger.log(`filename: ${archiveName}`);
    logger.log(`package size: ${formatFileSize(zipData.length)}`);
    logger.log(`unpacked size: ${formatFileSize(totalUnpackedSize)}`);
    logger.log(`shasum: ${shasum}`);
    logger.log(`total files: ${fileEntries.length}`);
    logger.log(`ignored (.mcpbignore) files: ${ignoredCount}`);

    logger.log(`\nOutput: ${finalOutputPath}`);
    return true;
  } catch (error) {
    if (error instanceof Error) {
      logger.error(`ERROR: Archive error: ${error.message}`);
    } else {
      logger.error("ERROR: Unknown archive error occurred");
    }
    return false;
  }
}
